Code Appendix
################################################################################
# Sizhe Zhang (sz592), Cynthia Zelga (cnz5)
# ECE 5725 Final Project: RPi Autonomous Guitar Tuner
# Monday lab section
# Dec 13, 2019
############################ SET-UP CODE START #################################
# Import necessary modules
import sys, pygame
import os # Used to display onto the piTFT
import time # Used for keeping track of time elapsed for while loop conditions
import pyaudio # Used to process audio input recorded by means of microphone
import numpy as np # Used for mathematical functions as well as the FFT algorithm
from pygame.locals import * # For event MOUSE variables
import servocontrol as sc # Module that includes set up of servo and defines servo control helper functions
import RPi.GPIO as GPIO # library for utilizing I/O pins, which was used to connect the servo motor to the RPi
# For displaying on piTFT or display
os.putenv('SDL_VIDEODRIVER','fbcon') # Display on piTFT
os.putenv('SDL_FBDEV','/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')
# Frequency Detection algorithm - adapted from one found online - link in References section!!
form_1 = pyaudio.paInt16 # 16-bit resolution
chans = 1 # 1 channel
samp_rate = 44100 # 44.1kHz sampling rate
chunk = 44100 # Samples for buffer (more samples = better freq resolution)
dev_index = 2 # Device index found by p.get_device_info_by_index(ii)
audio = pyaudio.PyAudio() # Create pyaudio instantiation
# Mic sensitivity correction and bit conversion
mic_sens_dBV = -47.0 # Mic sensitivity in dBV + any gain
mic_sens_corr = np.power(10.0,mic_sens_dBV/20.0) # Calculate mic sensitivity conversion factor
# Create pyaudio stream
stream = audio.open(format = form_1,rate = samp_rate,channels = chans, \
input_device_index = dev_index,input = True, \
frames_per_buffer=chunk)
# Initialize PyGame
pygame.init()
pygame.mouse.set_visible(False) # Set to True when debugging on monitor display, but False when running on piTFT
width = 320
height = 240
screen = pygame.display.set_mode((width,height)) # Setting piTFT screen dimensions
# Initialize colors used for graphics
WHITE = 255,255,255
BLACK = 0,0,0
PINK = 255,153,238
RED = 255,0,43
GREEN = 77,255,166
BLUE = 25,217,255
YELLOW = 229,255,102
PURPLE = 179,102,255
# Buttons that display on "choice_level" screen
# This is where the user selects which string they would like to tune. The screen
# features 6 different-colored buttons, labeled with the scientific pitch notation for
# each guitar string.
tuning_x = 80 # x-position of top left button
tuning_y = 80 # y-position of to left button
radius = 20 # Radius of each button
tuning_x_gap = 80 # Spacing in x-direction between buttons
tuning_y_gap = 80 # Spacing in y-direction between buttons
# Text variables
my_font = pygame.font.Font(None,30) # Font size
my_font_small = pygame.font.Font(None,20) # Font size - small
# Text for message that displays on the 1st screen
# Frequency-detection algorithm first listens and records background noise for 5 seconds
text_surface_noise1 = my_font_small.render('Please wait 5 seconds',True,WHITE) # First line of message
rect_noise1 = text_surface_noise1.get_rect(center=(160,120))
text_surface_noise2 = my_font_small.render('to record background noise... ',True,WHITE) # Second line of message
rect_noise2 = text_surface_noise2.get_rect(center=(160,140))
# Labels for buttons on "choice-level" screen; text displays scientific pitch notation for each guitar string
text_surface_E = my_font.render('E4',True,BLACK)
text_surface_B = my_font.render('B3',True,BLACK)
text_surface_G = my_font.render('G3',True,BLACK)
text_surface_D = my_font.render('D3',True,BLACK)
text_surface_A = my_font.render('A2',True,BLACK)
text_surface_E2 = my_font.render('E2',True,BLACK)
rect_E = text_surface_E.get_rect(center=(80,80)) # Top-left button
rect_B = text_surface_B.get_rect(center=(160,80))
rect_G = text_surface_G.get_rect(center=(240,80))
rect_D = text_surface_D.get_rect(center=(80,160))
rect_A = text_surface_A.get_rect(center=(160,160))
rect_E2 = text_surface_E2.get_rect(center=(240,160))
GPIO.setmode(GPIO.BCM) # Use Broadcom GPIO numbers system
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Set-up GPIO17 as an input, enable pullup resistor
#Define callback routine for quitting using physical quit button (GPIO17)
def GPIO17_callback(channel):
global program_quit # Variable used for quitting program
program_quit = True
global tuning # Variable used for while-loop used for "tuning" screen
tuning = False
global choice_level # Variable used for while-loop displaying "choice_level" screen
choice_level = False
# Define event for GPIO17 physical quit button, linking callback routine execution to a push of the button
GPIO.add_event_detect(17, GPIO.FALLING, callback=GPIO17_callback, bouncetime=300)
############################## SET-UP CODE END #################################
############################## MAIN CODE START #################################
# Initializing variable used for specificying direction of servo motor rotation
direction = 0 # 0 - stop, 1 - clockwise, 2 - counterclockwise, 3 - stop because tuning finished!
program_quit = False # Variable used for quitting program if True
# Used to set color of circle displayed on "tuning" screen to the color of the circle selected
# on "choice_level" screen
curr_color = BLACK
# While the program is running
while not program_quit:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
start_time = time.time() # Record start time
noise_fft_vec,noise_amp_vec = [],[]
ii = 0
noise_len = 5
# Loop for recording noise; displays message on screen telling user to wait 5 seconds for noise to be recorded
# Frequency detection algorithm adapted from online source - link in References section!!
while time.time()-start_time < 5 and not program_quit:
screen.fill(BLACK)
screen.blit(text_surface_noise1,rect_noise1)
screen.blit(text_surface_noise2,rect_noise2)
pygame.display.flip() # Display message on screen
# Record Noise
stream.start_stream()
data = np.fromstring(stream.read(chunk),dtype=np.int16)
if ii==noise_len:
data = data-noise_amp
data = ((data/np.power(2.0,15))*5.25)*(mic_sens_corr)
stream.stop_stream()
# Compute FFT
fft_data = (np.abs(np.fft.fft(data))[0:int(np.floor(4410/2))])/chunk
fft_data[1:] = 2*fft_data[1:]
# Calculate and subtract average spectral noise
if ii < noise_len:
if ii==0:
noise_fft_vec.append(fft_data)
noise_amp_vec.extend(data)
if ii==noise_len-1:
noise_fft = np.max(noise_fft_vec,axis=0)
noise_amp = np.mean(noise_amp_vec)
ii+=1
continue
fft_data = np.subtract(fft_data,noise_fft) # Subtract average spectral noise
# After recording noise for 5 seconds, display "choice_level" screen
# Displays the 6 different-colored buttons labeled with the sceintic pitch notation of each string
while not program_quit:
choice_level = True # Display "choice_level" screen
#Loop for choice of tuning
while choice_level:
# Draw screen contents
screen.fill(BLACK)
pygame.draw.circle(screen,RED,[80,80],30)
pygame.draw.circle(screen,YELLOW,[160,80],30)
pygame.draw.circle(screen,BLUE,[240,80],30)
pygame.draw.circle(screen,GREEN,[80,160],30)
pygame.draw.circle(screen,PINK,[160,160],30)
pygame.draw.circle(screen,PURPLE,[240,160],30)
screen.blit(text_surface_E,rect_E)
screen.blit(text_surface_B,rect_B)
screen.blit(text_surface_G,rect_G)
screen.blit(text_surface_D,rect_D)
screen.blit(text_surface_A,rect_A)
screen.blit(text_surface_E2,rect_E2)
dest_freq = 0 # Will be set to the value of the target frequency of the selected string
text_surface_quit = my_font_small.render("Quit",True,WHITE) # Quit button displayed on screen
rect_quit = text_surface_quit.get_rect(center=(300,20))
screen.blit(text_surface_quit,rect_quit)
# Touch detection
for event in pygame.event.get():
if event.type is MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos() # Touch detection
x,y = pos
# E4
if y > tuning_y-radius and y < tuning_y+radius:
if x > tuning_x-radius and x < tuning_x+radius:
dest_freq = 329.6
choice_level = False
curr_color = RED
# D3
if y > tuning_y-radius+tuning_y_gap and y < tuning_y+radius+tuning_y_gap:
if x > tuning_x-radius and x < tuning_x+radius:
dest_freq = 146.83
choice_level = False
curr_color = GREEN
# B3
if y > tuning_y-radius and y < tuning_y+radius:
if x > tuning_x-radius+tuning_x_gap and x < tuning_x+radius+tuning_x_gap:
dest_freq = 246.94
choice_level = False
curr_color = YELLOW
# A2
if y > tuning_y-radius+tuning_y_gap and y < tuning_y+radius+tuning_y_gap:
if x > tuning_x-radius+tuning_x_gap and x < tuning_x+radius+tuning_x_gap:
dest_freq = 110.00
choice_level = False
curr_color = PINK
# G3
if y > tuning_y-radius and y < tuning_y+radius:
if x > tuning_x-radius+tuning_x_gap*2 and x < tuning_x+radius+tuning_x_gap*2:
dest_freq = 196.00
choice_level = False
curr_color = BLUE
# E2
if y > tuning_y-radius+tuning_y_gap and y < tuning_y+radius+tuning_y_gap:
if x > tuning_x-radius+tuning_x_gap*2 and x < tuning_x+radius+tuning_x_gap*2:
dest_freq = 82.41
choice_level = False
curr_color = PURPLE
# Detect touch of quit button displayed on screen
if y > 0 and y < 30:
if x > 290 and x < 320:
program_quit = True
choice_level = False
tunning = False
choice_level =False
tuning = True # After a string has been selected, the "tuning" screen will be displayed next
new_sound = False # New detected sound initialized to False
pygame.display.flip()
curr_freq_d = dest_freq # Set curr_freq_d to dest_freq initially
reminder = "Please strum..." # This message will be displayed on the "tuning" screen
tuning_time = 0 # Initialized to 0; will be used to control how long servo motor will rotate for
direction = 0 # Initialized servo to stopped
# Loop for displaying "tuning" screen until the selected string has been successfully tuned
while tuning:
# Setting colors for circle that will be displayed to represent current frequency;
# color will be slightly lighter than center circle
# If current frequency is lower than target, circle will be drawn offset to the left
# If current frequency is higher than target, circle will be drawn offset to the right
rval = curr_color[0]*0.75
gval = curr_color[1]*0.75
bval = curr_color[2]*0.75
cval = rval, gval, bval
screen.fill(BLACK)
curr_x = int(( curr_freq_d - dest_freq )) + 160 # Sets x-pos of current frequency circle
# Draw current frequency circle
text_surface_curr_freq_d = my_font.render("Current frequency: "+str(curr_freq_d)+" Hz",True,WHITE)
rect_curr_freq_d = text_surface_curr_freq_d.get_rect(center=(160,80))
pygame.draw.circle(screen,cval,[curr_x,120],30)
screen.blit(text_surface_curr_freq_d,rect_curr_freq_d)
# Draw target frequency circle
text_surface_dest_freq = my_font.render(str(dest_freq),True,BLACK)
rect_dest_freq = text_surface_dest_freq.get_rect(center=(160,120))
pygame.draw.circle(screen,curr_color,[160,120],30)
screen.blit(text_surface_dest_freq,rect_dest_freq)
# Draw reminder message "to strum" onto screen
text_surface_reminder = my_font_small.render(reminder,True,WHITE)
rect_reminder = text_surface_reminder.get_rect(center=(160,180))
screen.blit(text_surface_reminder,rect_reminder)
# Starts listening for guitar strum (listens roughly every second)
stream.start_stream()
data = np.fromstring(stream.read(chunk),dtype=np.int16)
data = data-noise_amp # Subtracts out noise
data = ((data/np.power(2.0,15))*5.25)*(mic_sens_corr)
stream.stop_stream() # Stops listening
fft_data = (np.abs(np.fft.fft(data))[0:int(np.floor(chunk/2))])/chunk # Compute FFT
fft_data[1:] = 2*fft_data[1:]
f_vec = samp_rate*np.arange(chunk/2)/chunk
low_freq_loc = np.argmin(np.abs(f_vec))
curr_freq = np.argmax(fft_data[low_freq_loc:]) # Detected current frequency
# If the amplitude of the detected frequency is larger than noise amplitude-level limit and
# the difference in frequency is at least 2 Hz off and the current frequency is too low
if fft_data[curr_freq]>0.00001 and curr_freq-dest_freq < 2:
curr_freq_d = curr_freq # Current frequency detected
new_sound = True # New sound has been detected
# If current frequency is too low
if dest_freq-curr_freq > 2:
direction = 1 # Turn servo motor clockwise
if dest_freq-curr_freq>20:
tuning_time = 0.6 # Turn servo 1/2 a full rotation
else:
# Turn servo using ratio; 1 full rotation of servo was found to correspond to 20 Hz
tuning_time = 0.5*((dest_freq-curr_freq)/20)
elif curr_freq-dest_freq < 2: # Once the difference between frequencies is less than 2 Hz
tuning = False # Tuning is complete!
tuning_time = 0
direction = 3 # Set servo motor to stopped because tuned!
screen.fill(BLACK)
curr_x = int(( curr_freq_d - dest_freq )) + 160 # Update position of current frequency circle
# Draw current frequency circle
text_surface_curr_freq_d = my_font.render("Current frequency: "+str(curr_freq_d)+" Hz",True,WHITE)
rect_curr_freq_d = text_surface_curr_freq_d.get_rect(center=(160,80))
pygame.draw.circle(screen,cval,[curr_x,120],30)
screen.blit(text_surface_curr_freq_d,rect_curr_freq_d)
# Draw target frequency circle
text_surface_dest_freq = my_font.render(str(dest_freq),True,BLACK)
rect_dest_freq = text_surface_dest_freq.get_rect(center=(160,120))
pygame.draw.circle(screen,curr_color,[160,120],30)
screen.blit(text_surface_dest_freq,rect_dest_freq)
# Draw reminder "to strum" onto screen
text_surface_reminder = my_font_small.render(reminder,True,WHITE)
rect_reminder = text_surface_reminder.get_rect(center=(160,180))
screen.blit(text_surface_reminder,rect_reminder)
if new_sound:
start_time = time.time() # Record start time if new sound detected
if direction == 1 and time.time()-start_time < tuning_time: # Turn clockwise
reminder = "Please wait"
# Call helper function to turn servo clockwise for tuning_time amount of seconds
sc.halfSpeed("cw", tuning_time)
# Added a small delay as guitar string strums have reverb;
# this ensures same string is not detected again
time.sleep(0.75)
elif direction == 3: # Tuning is done!
reminder = "Finished tuning!"
# Redraw screen with finished state
time_back = time.time()
while time.time()-time_back < 4:
screen.fill(BLACK)
curr_x = int(( curr_freq_d - dest_freq )) + 160
# Draw message notifying user that string is tuned!! This displays for 4 seconds
text_surface_reminder = my_font.render(reminder,True,WHITE)
rect_reminder = text_surface_reminder.get_rect(center=(160,180))
screen.blit(text_surface_reminder,rect_reminder)
pygame.display.flip()
else: # Otherwise, no sound was detected, so tell the user to strum again
direction = 0 # Servo motor should be stopped
reminder = "Please strum..."
new_sound = False # Reset
# Touch detection for go back or quit program
text_surface_back = my_font_small.render("Back",True,WHITE)
rect_back = text_surface_back.get_rect(center=(300,220))
screen.blit(text_surface_back,rect_back)
text_surface_quit = my_font_small.render("Quit",True,WHITE)
rect_quit = text_surface_quit.get_rect(center=(300,20))
screen.blit(text_surface_quit,rect_quit)
for event in pygame.event.get():
if event.type is MOUSEBUTTONDOWN:
pos = pygame.mouse.get_pos() # Touch detection
x,y = pos
# Detect touch of "back" button
if y > 200 and y < 240:
if x > 280 and x < 320:
tuning = False
# Detect touch of "quit" button
if y > 0 and y < 30:
if x > 290 and x < 320:
program_quit = True
tuning = False
pygame.display.flip()